import axios, { AxiosError, AxiosInstance, AxiosPromise, AxiosRequestConfig, AxiosResponse } from 'axios'
import axiosRetry from 'axios-retry'
import { isObject } from 'lodash'
import qs from 'qs'
import { E_ERROR_CODE, E_HTTP_CODE } from '../../typing/enum/error-code'
import { AnyObject, IDataWithError, IResponseData } from '../../typing/service-typing'
import { formatURL } from '../utils/url'
import { interceptorHeader } from './interceptors/header-interceptor'
import { interceptorLog } from './interceptors/log-interceptor'
import { E_HTTP_METHODS, IURL } from './transfer-typing'

export interface IHttpTransferConfig {
  isWithSearchBoost?: boolean
  urlParam?: AnyObject
  queryParam?: AnyObject
  isFromTransfer?: boolean
}

export type ITransferConfig = IHttpTransferConfig & AxiosRequestConfig
type IErrorHandler = (error: AxiosError) => void

const REQUEST_TIMEOUT = 10000

export class HttpTransfer {
  private http: AxiosInstance
  private errorHandler: IErrorHandler | null = null
  private token: string = ''

  constructor(baseURL: string) {
    this.http = axios.create({
      timeout: REQUEST_TIMEOUT,
      baseURL,
      withCredentials: true,
      headers: {
        'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
        'Content-type': 'application/json'
      },
      paramsSerializer(params: AnyObject): string {
        return qs.stringify(params, { arrayFormat: 'repeat' })
      }
    })

    axiosRetry(this.http, {
      retryCondition: (error: AxiosError) => {
        return !error.response
      },
      retryDelay: (retryCount: number) => {
        return retryCount * 1000
      }
    })

    this.addInterceptor()
  }

  private addInterceptor(): void {
    interceptorHeader(this.http)
    interceptorLog(this.http)
  }

  get transfer(): AxiosInstance {
    return this.http
  }

  set transfer(http: AxiosInstance) {
    this.http = http
  }

  setToken(token: string) {
    this.token = token
  }

  setErrorHandler(handler: IErrorHandler): void {
    this.errorHandler = handler
  }

  async request<T>(urlInfo: IURL, data?: unknown, config: ITransferConfig = {}): Promise<IDataWithError<T>> {
    let { url } = urlInfo
    const { method, noToken } = urlInfo

    const { urlParam } = config
    if (urlParam) {
      url = formatURL(url, urlParam)
    }

    if (!noToken) {
      if (data === undefined || data == null) {
        data = {
          token: this.token
        }
      } else if (isObject(data)) {
        data = {
          ...data,
          token: this.token
        }
      }
    }

    const p: AxiosPromise = this.send(method, url, data, config)

    return this.errorWrapper(
      url,
      p.then(resp => {
        return resp
      })
    )
  }

  private send(method: E_HTTP_METHODS, url: string, data?: unknown, config: ITransferConfig = {}): AxiosPromise {
    let p: AxiosPromise
    if (method === E_HTTP_METHODS.GET) {
      config.params = data
      p = this.http.get(url, config)
    } else if (method === E_HTTP_METHODS.POST || method === E_HTTP_METHODS.FORM) {
      p = this.http.post(url, data, config)
    } else if (method === E_HTTP_METHODS.PUT) {
      p = this.http.put(url, data, config)
    } else if (method === E_HTTP_METHODS.DELETE) {
      config.data = data
      p = this.http.delete(url, config)
    } else {
      throw new Error('Invalid method')
    }

    return p
  }

  private async errorWrapper<T>(url: string, p: AxiosPromise): Promise<IDataWithError<T>> {
    return p
      .then((data: AxiosResponse) => {
        const resp = data.data as IResponseData<T>
        return {
          errorCode: resp.code,
          errorMsg: resp.msg,
          data: resp.data,
          requestId: resp.request_id,
          timestamp: resp.timestamp
        }
      })
      .catch((error: AxiosError) => {
        if (this.errorHandler) {
          this.errorHandler(error)
        }
        let errorCode: number
        let httpCode: number
        let errorData
        if (error.code === 'ENOTFOUND') {
          // Server is not reachable
          errorCode = E_ERROR_CODE.SERVER_UNREACHABLE
          httpCode = E_HTTP_CODE.SERVER_UNREACHABLE
        } else if (error.code === 'ECONNABORTED') {
          // Request is canceled or timeout
          errorCode = E_ERROR_CODE.REQUEST_CANCELED
          httpCode = E_HTTP_CODE.REQUEST_CANCELED
        } else {
          const response = error.response
          if (!response) {
            errorCode = E_ERROR_CODE.REQUEST_FAILED
            httpCode = E_HTTP_CODE.NETWORK_ERROR
          } else {
            const status: number = response.status
            errorData = response.data
            httpCode = status
            if (status >= E_HTTP_CODE.INTERNAL_SERVER_ERROR) {
              errorCode = E_ERROR_CODE.SERVER_ERROR
            } else {
              const code: number = response.data.code
              if (code) {
                errorCode = code
              } else {
                errorCode = E_ERROR_CODE.REQUEST_FAILED
              }
            }
          }
        }

        console.log('transfer error: ', {
          errorCode,
          httpCode,
          errorData
        })

        const result: IDataWithError = {
          errorCode,
          error: {
            httpCode,
            errorData
          }
        }

        return result
      })
  }
}
